﻿//////////////////////////////////////////////////////////////////////////////
//This file is part of My Nes                                               //
//A Nintendo Entertainment System Emulator.                                 //
//                                                                          //
//Copyright © 2009 Ala Hadid (AHD)                                          //
//                                                                          //
//My Nes is free software; you can redistribute it and/or modify            //
//it under the terms of the GNU General Public License as published by      //
//the Free Software Foundation; either version 2 of the License, or         //
//(at your option) any later version.                                       //
//                                                                          //
//My Nes is distributed in the hope that it will be useful,                 //
//but WITHOUT ANY WARRANTY; without even the implied warranty of            //
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             //
//GNU General Public License for more details.                              //
//                                                                          //
//You should have received a copy of the GNU General Public License         //
//along with this program; if not, write to the Free Software               //
//Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA//
//////////////////////////////////////////////////////////////////////////////
/*
 This version started at : Friday, October 2, 2009
 */
/* **********************************************
 * Some Code taken from : 
 * SharpNes, Copyright (c) 2005, Jonathan Turner
 * All rights reserved.
 * **********************************************
 * Classes taken
 * 1 NesEmulator : Not everything, threading and memory access
 * 2 Mappers : everything
 * 3 CPU6502 : some of it from SharpNes
 * 4 Cart : everything
 */
using System;
using System.IO;
using System.Threading;

namespace AHD.MyNes.Nes
{
    using System.Windows.Controls;
    using System.Windows.Media.Imaging;
    using Sine;

        public enum NesSystem
    {
        NTSC, PAL
    }
    /*
     * TODO: save state
     */

    public class NesEmulator
    {
        /// <summary>
        /// Nes emulater main !!
        /// </summary>
        /// <param name="TV">The control that will be a draw surface</param>
        /// <param name="SoundDevice">The sound device (WARNING !! : don't make TV = SoundDevice)</param>
        public NesEmulator(Image TV, NesMediaStreamSource SoundDevice)
        {
            Vid = new Video(TV);
            myapu = new APU(SoundDevice, this);
            InitializeEngine();
        }
        public uint Ticks_Per_Scanline = 113;
        //Devices in the nes
        public Cart myCartridge;// Nes cart
        public CPU6502 my6502;// CPU
        public Mappers myMapper;//Current mapper
        public PPU myPPU;//PPU
        public Joypad myJoypad;//Joypads
        public Video Vid;//Video renderer
        public APU myapu;//sound
        public int FPS = 50;

        NesSystem _CurrentSystem = NesSystem.PAL;
        public NesSystem CurrentSystem
        {
            get { return _CurrentSystem; }
            set
            {
                _CurrentSystem = value;
                switch (value)
                {
                    case NesSystem.NTSC:
                        Ticks_Per_Scanline = 113;
                        myapu.SetClockSpeed(1789772);
                        myapu._frameCycles = 29606;//262 * 113
                        myapu._palTiming = false;
                        myPPU.FramePeriod = 0.01667;//60 FPS
                        myPPU.Scanlinesperframe = 262;
                        //////////////////////////////
                        //This value is important, in doc's, they say
                        //that VBLANK is happens in SL (scan line) # 240
                        //but in real test (with some roms like Castlequest),
                        //this No will make a bad scroll in the game ...
                        //however i test it and 248 (with NTSC) is verygood
                        //for this game and others (Castlequest, Castlevania ..)
                        //remember that the PPU return VBLANK in SL # 240
                        //in the status register
                        //while the real one happens after that lil' bit.
                        myPPU.ScanlinesOfVBLANK = 248;
                        //////////////////////////////
                        Vid.FirstLinesTCut = 2048;//256 * 8
                        break;
                    case NesSystem.PAL:
                        Ticks_Per_Scanline = 106;
                        myapu.SetClockSpeed(1773447);

                        myapu._frameCycles = 33072;//312 * 106
                        myapu._palTiming = true;
                        myPPU.FramePeriod = 0.020;//50 FPS
                        myPPU.Scanlinesperframe = 312;
                        myPPU.ScanlinesOfVBLANK = 290;
                        Vid.FirstLinesTCut = 0;
                        break;
                }

                myapu.SetClockSpeed((int)Math.Round(Ticks_Per_Scanline * (1 / myPPU.FramePeriod) * myPPU.Scanlinesperframe));
            }
        }
        //Fields
        public bool isQuitting;
        public bool hasQuit;
        public bool isDebugging;
        public bool isSaveRAMReadonly;
        public bool isPaused;
        public bool fix_bgchange;
        public bool fix_spritehit;
        public bool fix_scrolloffset1;
        public bool fix_scrolloffset2;
        public bool fix_scrolloffset3;
        public bool LimitTo50 = false;
        public bool LimitTo60 = false;
        public byte[][] scratchRam;
        public byte[] saveRam;
        public bool SoundEnabled = true;
        string saveFilename;
        public string FileName = "";
        public uint numOfInstructions;
        private int JoyData1 = 0;
        private int JoyData2 = 0;
        private byte JoyStrobe = 0;
        public bool SpeedThrottling = false;
        //Memory
        public byte ReadMemory8(ushort address)
        {
            byte returnvalue = 0;
            if (address < 0x2000)
            {
                if (address < 0x800)
                {
                    returnvalue = scratchRam[0][address];
                }
                else if (address < 0x1000)
                {
                    returnvalue = scratchRam[1][address - 0x800];
                }

                else if (address < 0x1800)
                {
                    returnvalue = scratchRam[2][address - 0x1000];
                }
                else
                {
                    returnvalue = scratchRam[3][address - 0x1800];
                }
            }
            else if (address < 0x6000)
            {
                switch (address)
                {
                    case (0x2002): returnvalue = myPPU.Status_Register_Read(); break;
                    case (0x2004): returnvalue = myPPU.SpriteRam_IO_Register_Read(); break;
                    case (0x2007): returnvalue = myPPU.VRAM_IO_Register_Read(); break;
                    case (0x4015): returnvalue = myapu.ReadStatusReg(); break;
                    case (0x4016):
                        byte num2 = (byte)(JoyData1 & 1);
                        JoyData1 = JoyData1 >> 1;
                        returnvalue = num2;
                        break;
                    case (0x4017):
                        byte num3 = (byte)(JoyData2 & 1);
                        JoyData2 = JoyData2 >> 1;
                        returnvalue = num3;
                        break;
                }
            }
            else if (address < 0x8000)
            {
                returnvalue = saveRam[address - 0x6000];
                if (myCartridge.mapper == 5)
                    returnvalue = 1;
            }
            else
            {
                returnvalue = myMapper.ReadPrgRom(address);
            }
            return returnvalue;
        }
        public byte WriteMemory8(ushort address, byte data)
        {
            if (address < 0x800)
            {
                scratchRam[0][address] = data;
            }
            else if (address < 0x1000)
            {
                scratchRam[1][address - 0x800] = data;
            }
            else if (address < 0x1800)
            {
                scratchRam[2][address - 0x1000] = data;
            }
            else if (address < 0x2000)
            {
                scratchRam[3][address - 0x1800] = data;
            }
            else if (address < 0x4000)
            {
                switch (address)
                {
                    case (0x2000): myPPU.Control_Register_1_Write(data); break;
                    case (0x2001): myPPU.Control_Register_2_Write(data); break;
                    case (0x2003): myPPU.SpriteRam_Address_Register_Write(data); break;
                    case (0x2004): myPPU.SpriteRam_IO_Register_Write(data); break;
                    case (0x2005): myPPU.VRAM_Address_Register_1_Write(data); break;
                    case (0x2006): myPPU.VRAM_Address_Register_2_Write(data); break;
                    case (0x2007): myPPU.VRAM_IO_Register_Write(data); break;
                }
            }
            else if (address < 0x6000)
            {
                switch (address)
                {
                    case (0x4000): myapu.WriteRectReg1(0, data); break;
                    case (0x4001): myapu.WriteRectReg2(0, data); break;
                    case (0x4002): myapu.WriteRectReg3(0, data); break;
                    case (0x4003): myapu.WriteRectReg4(0, data); break;
                    case (0x4004): myapu.WriteRectReg1(1, data); break;
                    case (0x4005): myapu.WriteRectReg2(1, data); break;
                    case (0x4006): myapu.WriteRectReg3(1, data); break;
                    case (0x4007): myapu.WriteRectReg4(1, data); break;
                    case (0x4008): myapu.WriteTriReg1(data); break;
                    case (0x4009): myapu.WriteTriReg2(data); break;
                    case (0x400A): myapu.WriteTriReg3(data); break;
                    case (0x400B): myapu.WriteTriReg4(data); break;
                    case (0x400C): myapu.WriteNoiseReg1(data); break;
                    case (0x400E): myapu.WriteNoiseReg2(data); break;
                    case (0x400F): myapu.WriteNoiseReg3(data); break;
                    case (0x4010): myapu.WriteDMCReg1(data); break;
                    case (0x4011): myapu.WriteDMCReg2(data); break;
                    case (0x4012): myapu.WriteDMCReg3(data); break;
                    case (0x4013): myapu.WriteDMCReg4(data); break;
                    case (0x4014): myPPU.SpriteRam_DMA_Begin(data); break;
                    case (0x4015): myapu.WriteStatusReg(data); break;
                    case (0x4016):
                        if ((this.JoyStrobe == 1) && ((data & 1) == 0))
                        {
                            this.JoyData1 = myJoypad.GetJoyData1() | 0x100;
                            this.JoyData2 = myJoypad.GetJoyData2() | 0x200;
                        }
                        this.JoyStrobe = (byte)(data & 1);
                        break;
                }

                if (myCartridge.mapper == 5)
                    myMapper.WritePrgRom(address, data);
            }
            else if (address < 0x8000)
            {
                if (!isSaveRAMReadonly)
                    saveRam[address - 0x6000] = data;

                if (myCartridge.mapper == 34 & myCartridge.mapper == 16)
                    myMapper.WritePrgRom(address, data);

            }
            else
            {
                myMapper.WritePrgRom(address, data);
            }
            return 1;
        }
        public ushort ReadMemory16(ushort address)
        {
            byte data_1 = 0;
            byte data_2 = 0;
            if (address < 0x2000)
            {
                if (address < 0x800)
                {
                    data_1 = scratchRam[0][address];
                    data_2 = scratchRam[0][address + 1];
                }
                else if (address < 0x1000)
                {
                    data_1 = scratchRam[1][address - 0x800];
                    data_2 = scratchRam[1][address - 0x800 + 1];
                }

                else if (address < 0x1800)
                {
                    data_1 = scratchRam[2][address - 0x1000];
                    data_2 = scratchRam[2][address - 0x1000 + 1];
                }
                else
                {
                    data_1 = scratchRam[3][address - 0x1800];
                    data_2 = scratchRam[3][address - 0x1800 + 1];
                }
            }
            else if (address < 0x8000)
            {
                data_1 = saveRam[address - 0x6000];
                data_2 = saveRam[address - 0x6000 + 1];
            }
            else
            {
                data_1 = myMapper.ReadPrgRom(address);
                data_2 = myMapper.ReadPrgRom((ushort)(address + 1));
            }
            ushort data = (ushort)((data_2 << 8) + data_1);
            return data;
        }


        public double LastAudioTime
        {
            get { return myPPU._lastAudioFrame; }
        }
        public double LastFameTime
        {
            get { return myPPU._lastFrameTime; }
        }
        public double CurrentFameTime
        {
            get { return myPPU._currentFrameTime; }
        }
        /// <summary>
        /// Start the nes machine
        /// </summary>
        public void InitializeEngine()
        {
            myCartridge = new Cart();
            my6502 = new CPU6502(this);
            myMapper = new Mappers(this, ref myCartridge);
            myPPU = new PPU(this, Vid);
            myJoypad = new Joypad();

            myapu.Reset();

            scratchRam = new byte[4][];
            scratchRam[0] = new byte[0x800];
            scratchRam[1] = new byte[0x800];
            scratchRam[2] = new byte[0x800];
            scratchRam[3] = new byte[0x800];
            saveRam = new byte[0x2000];

            isSaveRAMReadonly = false;
            isDebugging = false;
            isQuitting = false;
            isPaused = false;
            hasQuit = false;
            fix_bgchange = false;
            //fix_spritehit = false;
            fix_scrolloffset1 = false;
            fix_scrolloffset2 = false;
            fix_scrolloffset3 = false;
        }
        /// <summary>
        /// Restart !!
        /// </summary>
        public void RestartEngine()
        {
            isSaveRAMReadonly = false;
            isDebugging = false;
            isQuitting = false;
            isPaused = false;
            hasQuit = false;
            fix_bgchange = false;
            //fix_spritehit = false;
            fix_scrolloffset1 = false;
            fix_scrolloffset2 = false;
            fix_scrolloffset3 = false;
            myPPU.RestartPPU();
        }
        /// <summary>
        /// Quit (turn off)
        /// </summary>
        public void QuitEngine()
        {
            myapu.Pause();
            myapu.Shutdown();
            isQuitting = true; 
        }
        public void TogglePause()
        {
            if (isPaused)
            {
                isPaused = false;
            }
            else
            {
                isPaused = true;
            }
        }

        public void PreFrame()
        {
            my6502.pc_register = ReadMemory16(0xFFFC);
        }
        

        public void DoFrame()
        {
            my6502.RunProcessor();
        }
        
        //Cart ...
        public bool LoadCart(Stream reader)
        {
            reader.Seek(0, SeekOrigin.Begin);
            byte[] nesHeader = new byte[16];
            int i;
            try
            {
                    reader.Read(nesHeader, 0, 16);
                    int prg_roms = nesHeader[4] * 4;
                    myCartridge.prg_rom_pages = nesHeader[4];
                    myCartridge.prg_rom = new byte[prg_roms][];
                    for (i = 0; i < (prg_roms); i++)
                    {
                        myCartridge.prg_rom[i] = new byte[4096];
                        reader.Read(myCartridge.prg_rom[i], 0, 4096);
                    }
                    int chr_roms = nesHeader[5] * 8;
                    myCartridge.chr_rom_pages = nesHeader[5];
                    if (myCartridge.chr_rom_pages != 0)
                    {
                        myCartridge.chr_rom = new byte[chr_roms][];
                        for (i = 0; i < (chr_roms); i++)
                        {
                            myCartridge.chr_rom[i] = new byte[1024];
                            reader.Read(myCartridge.chr_rom[i], 0, 1024);
                        }
                        myCartridge.is_vram = false;
                    }
                    else
                    {
                        myCartridge.chr_rom = new byte[(prg_roms * 16)][];
                        for (i = 0; i < (prg_roms * 16); i++)
                        {
                            myCartridge.chr_rom[i] = new byte[1024];
                        }
                        myCartridge.is_vram = true;
                    }
                    if ((nesHeader[6] & 0x1) == 0x0)
                    {
                        myCartridge.mirroring = MIRRORING.HORIZONTAL;
                    }
                    else
                    {
                        myCartridge.mirroring = MIRRORING.VERTICAL;
                    }

                    if ((nesHeader[6] & 0x2) == 0x0)
                    {
                        myCartridge.save_ram_present = false;
                    }
                    else
                    {
                        myCartridge.save_ram_present = true;
                    }

                    if ((nesHeader[6] & 0x4) == 0x0)
                    {
                        myCartridge.trainer_present = false;
                    }
                    else
                    {
                        myCartridge.trainer_present = true;
                    }

                    if ((nesHeader[6] & 0x8) != 0x0)
                    {
                        myCartridge.mirroring = MIRRORING.FOUR_SCREEN;
                    }

                    if (nesHeader[7] == 0x44)
                    {
                        myCartridge.mapper = (byte)(nesHeader[6] >> 4);
                    }
                    else
                    {
                        myCartridge.mapper = (byte)((nesHeader[6] >> 4) + (nesHeader[7] & 0xf0));
                    }
                    if ((nesHeader[6] == 0x23) && (nesHeader[7] == 0x64))
                        myCartridge.mapper = 2;
                    if ((myCartridge.prg_rom[prg_roms - 1][0xfeb] == 'Z') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfec] == 'E') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfed] == 'L') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfee] == 'D') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfef] == 'A'))
                    {
                        fix_bgchange = true;
                    }
                    if ((myCartridge.prg_rom[prg_roms - 1][0xfe0] == 'B') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe1] == 'B') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe2] == '4') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe3] == '7') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe4] == '9') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe5] == '5') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe6] == '6') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe7] == '-') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe8] == '1') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfe9] == '5') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfea] == '4') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfeb] == '4') &&
                        (myCartridge.prg_rom[prg_roms - 1][0xfec] == '0'))
                    {
                        fix_scrolloffset1 = true;
                    }
                    if ((myCartridge.prg_rom[0][0x9] == 0xfc) &&
                        (myCartridge.prg_rom[0][0xa] == 0xfc) &&
                        (myCartridge.prg_rom[0][0xb] == 0xfc) &&
                        (myCartridge.prg_rom[0][0xc] == 0x40) &&
                        (myCartridge.prg_rom[0][0xd] == 0x40) &&
                        (myCartridge.prg_rom[0][0xe] == 0x40) &&
                        (myCartridge.prg_rom[0][0xf] == 0x40))
                    {
                        fix_scrolloffset2 = true;
                    }
                    if ((myCartridge.prg_rom[0][0x75] == 0x11) &&
                        (myCartridge.prg_rom[0][0x76] == 0x12) &&
                        (myCartridge.prg_rom[0][0x77] == 0x13) &&
                        (myCartridge.prg_rom[0][0x78] == 0x14) &&
                        (myCartridge.prg_rom[0][0x79] == 0x07) &&
                        (myCartridge.prg_rom[0][0x7a] == 0x03) &&
                        (myCartridge.prg_rom[0][0x7b] == 0x03) &&
                        (myCartridge.prg_rom[0][0x7c] == 0x03) &&
                        (myCartridge.prg_rom[0][0x7d] == 0x03)
                        )
                    {
                        fix_scrolloffset3 = true;
                    }
                    myMapper.SetUpMapperDefaults();
                
            }
            catch
            {
                return false;
            }

            if (myCartridge.save_ram_present)
            {
                
            }
            return true;
        }
    }

}
